// 
//   Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
//  
//   This program is free software: you can redistribute it and/or modify
//   it under the terms of the GNU Affero General Public License as
//   published by the Free Software Foundation, either version 3 of the
//   License, or (at your option) any later version.
//  
//   This program is distributed in the hope that it will be useful,
//   but WITHOUT ANY WARRANTY; without even the implied warranty of
//   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//   GNU Affero General Public License for more details.
//  
//   You should have received a copy of the GNU Affero General Public License
//   along with this program.  If not, see <http://www.gnu.org/licenses/>.
//  
//  


using System;
using System.Collections.Generic;

using Anculus.Core;
using Galaxium.Protocol.Xmpp.Library.Xml;
using Galaxium.Protocol.Xmpp.Library.Core;
using Galaxium.Protocol.Xmpp.Library.Extensions;

namespace Galaxium.Protocol.Xmpp.Library.Streams
{
	public class StreamListener: IAttachable
	{
		Client IAttachable.Client { get { return _client; }}
		
		private Client _client;
		private List<string> _manager_seq;
		private Dictionary<string, IBytestreamManager> _managers;
		private Dictionary<string, IStreamProfileListener> _listeners;
		
		public StreamListener ()
		{
			_manager_seq = new List<string> ();
			_managers = new Dictionary<string, IBytestreamManager> ();
			_listeners = new Dictionary<string, IStreamProfileListener> ();
		}
		
		public void AddBytestreamMethod (IBytestreamManager manager)
		{
			if (_client != null && manager.Client == null)
				manager.Attach (_client);
			else if (_client != manager.Client)
				throw new ArgumentException ("Manager doesn't belong to the same client.");
			
			_managers.Add (manager.Identifier, manager);
			_manager_seq.Add (manager.Identifier);
		}
		
		public void RemoveBytestreamMethod (string method)
		{
			_managers.Remove (method);
			_manager_seq.Remove (method);
		}
		
		public void AddProfileListener (IStreamProfileListener listener)
		{
			_listeners.Add (listener.ProfileNamespace, listener);
			if (_client != null)
				_client.RegisterFeature (listener.ProfileNamespace);
		}
		
		public void RemoveProfileListener (string profile)
		{
			_listeners.Remove (profile);
			if (_client != null)
				_client.UnregisterFeature (profile);
		}
		
		private void HandleQuery (Iq query)
		{
			var thread = new System.Threading.Thread (() => RealHandleQuery (query));
			thread.IsBackground = true;
			thread.Start ();
		}
		
		private void RealHandleQuery (Iq query)
		{
			var q = query.Query;
			
			var uid = query.From;
			var sid = q ["id"];
			var profile = q ["profile"];
			var mime_type = q ["mime-type"];
			List<string> methods;
			
			// Get offerred stream methods
			
			try {
				var features = q.FirstChild ("feature", Namespaces.FeatureNegotiation);
				var form = new DataForm (features.FirstChild ("x", Namespaces.DataForms), null);
				methods = new List<string> ();
				foreach (var option in form ["stream-method"].GetOptions ()) {
					if (_managers.ContainsKey (option.Value))
						methods.Add (option.Value);
				}
				if (methods.Count == 0)
					throw new NotSupportedException ("No supported stream methods received.");
			}
			catch (Exception e) {
				Log.Error (e, "Error parsing stream method");
				var response = query.CreateErrorResponse ("bad-request");
				var app_specific = new Element ("no-valid-streams", Namespaces.StreamInitiation);
				response.FirstChild ("error", null).AppendChild (app_specific);
				_client.Send (response);
				return;
			}
			
			// Let appropriate profile handler process the request
			
			var data = q.FirstChild (null, profile);
			
			IStreamProfileListener listener;
			if (_listeners.TryGetValue (profile, out listener)) {
				RequestResult result;
				
				try {
					result = listener.HandleRequest (uid, sid, mime_type, methods, data);
				}
				catch (QueryException e) {
					var response = query.Response ();
					response.Type = IqType.Error;
					response.AppendChild (e.ToXml ());
					_client.Send (response);
					return;
				}
				
				if (result.Accepted) {
					_managers [result.Method].ExpectStream (uid, sid, (rslt, stream) => {
						if (rslt == StreamRequestResult.Success)
							Log.Info ("Stream " + sid + " to entity " + uid + " successfuly opened.");
						else Log.Error ("Stream opening failed. (" + rslt.ToString () + ")"); 
						result.Handler (rslt, stream);
					});
					
					var response = query.Response ();
					var si = new Element ("si", Namespaces.StreamInitiation);
					si.AppendChild (result.ProfileData);
					si.AppendChild (CreateFeatureChild (result.Method));
					response.AppendChild (si);
					_client.Send (response);
				}
				else {
					var response = query.CreateErrorResponse ("forbidden", result.DeclineReason);
					_client.Send (response);
				}
			}
			else {
				var response = query.CreateErrorResponse ("bad-request");
				var app_specific = new Element ("bad-profile", Namespaces.StreamInitiation);
				response.FirstChild ("error", null).AppendChild (app_specific);
				_client.Send (response);
			}
		}
		
		private Element CreateFeatureChild (string method)
		{
			var form = new DataForm (FormAction.Submit, null);
			form.AddField ("stream-method", method, null);
			var feat = new Element ("feature", Namespaces.FeatureNegotiation);
			feat.AppendChild (form.ToXml (true));
			return feat;
		}
		
		public void Attach (Client client)
		{
			_client = client;
			_client.DefineSetQueryHandler (Namespaces.StreamInitiation, HandleQuery);
			foreach (var profile in _listeners.Keys)
				_client.RegisterFeature (profile);
			foreach (var manager in _managers.Values)
				if (manager.Client == null) manager.Attach (_client);
			_client.RegisterFeature (Namespaces.StreamInitiation);
		}
		
		public void Detach ()
		{
			_client.UnregisterFeature (Namespaces.StreamInitiation);
			_client.DefineSetQueryHandler (Namespaces.StreamInitiation, null);
			foreach (var profile in _listeners.Keys)
				_client.UnregisterFeature (profile);
			foreach (var method in _manager_seq.ToArray ())
				RemoveBytestreamMethod (method);
			_client = null;
		}
		
		public void DetachAll ()
		{
			foreach (var manager in _managers.Values)
				manager.Detach ();
			Detach ();
		}
	}
}
